import * as step from '@flow-step/step-toolkit'
import process from "process"
import fs from "fs"
import * as sinon from 'sinon'
import {expect} from 'chai'
// @ts-ignore
import * as install from "../src/install"
import {isCnpmOrPnpmInstalled,installYarn} from '../src/install'
import {platform} from '@flow-step/step-toolkit'
import path from 'path'

let sandbox = sinon.createSandbox()
let addPath: sinon.SinonStub
let stepInstall: sinon.SinonStub
let stepFormatToLatestPatchVersion: sinon.SinonStub
let executeCommand: sinon.SinonStub
let executeCommandOutput: sinon.SinonStub
let stepInfo: sinon.SinonStub
let rmSync: sinon.SinonStub
let symlinkSync: sinon.SinonStub
let env: NodeJS.ProcessEnv;
let existsSyncStub: sinon.SinonStub;
let copyFileSyncStub: sinon.SinonStub;

beforeEach(() => {
    sandbox.stub(process, 'env').value({
        ...process.env,
        FLOW_TOOL_REGISTRY_URL: 'https://tool-registry-url.com',
        FLOW_RUNNER_TOOL_CACHE_DIR: '/tmp/flow_tools'
    })
    env = { ...process.env };

    addPath = sandbox.stub(step, 'addPath')
    stepInstall = sandbox.stub(step.tool, 'install')
    stepFormatToLatestPatchVersion = sandbox.stub(step.tool, 'formatToLatestPatchVersion')
    executeCommand = sandbox.stub(step.exec, 'call').resolves()
    executeCommandOutput = sandbox.stub(step.exec, 'callOutput').resolves()
    stepInfo = sandbox.stub(step, 'info')
    rmSync = sandbox.stub(fs, 'rmSync')
    symlinkSync = sandbox.stub(fs, 'symlinkSync')
    existsSyncStub = sandbox.stub(fs, 'existsSync');
    copyFileSyncStub = sandbox.stub(fs, 'copyFileSync');
})

afterEach(() => {
    process.env.FLOW_RUNNER_TOOL_CACHE_DIR = ''
    process.env = env;
    sandbox.restore()
})

describe('symlinkInstallPathToCachePath', () => {
    it('when installPath is undefined', () => {
        const nodeCachedPath: string = "/tmp/flow_tools"
        install.symlinkInstallPathToCachePath(undefined, nodeCachedPath)
        sinon.assert.notCalled(rmSync)
        sinon.assert.notCalled(symlinkSync)
    })

    it('when installPath is not undefined', () => {
        const nodeCachedPath: string = "/tmp/flow_tools"
        const installPath: string = "/srv/node/19.0.1"
        install.symlinkInstallPathToCachePath(installPath, nodeCachedPath)
        sinon.assert.calledOnceWithExactly(rmSync, installPath, {recursive: true, force: true})
        sinon.assert.calledOnceWithMatch(symlinkSync, nodeCachedPath, installPath, 'dir')
    })
})

describe('install node', () => {
    it('install node when input node version is not existed', async () => {
        let inputNodeVersion = 'undefined'
        stepFormatToLatestPatchVersion.rejects()
        try {
            await install.installNode(inputNodeVersion, 'cnpm',undefined);
            expect.fail("Expected error not thrown");
        } catch (err) {
            expect(true).to.be.true;
        }
    })

    const nodeCacheDir = "/tmp/flow_tools/node/v14.8.3/x64";
    it('install node when input node version is existed without cachedVersion and installPath is undefined', async () => {
        sinon.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_LINUX)
        let inputNodeVersion = '14.8'
        stepFormatToLatestPatchVersion.returns('v14.8.3')
        stepInstall.returns(nodeCacheDir)
        await install.installNode(inputNodeVersion, 'cnpm', undefined)
        sinon.assert.calledWith(addPath, `${nodeCacheDir}/bin`)
        sinon.assert.calledTwice(addPath)
        sinon.restore()
    })

    it('install node when input node version is support without cachedVersion and installPath is not undefined', async () => {
        let inputJdkVersion = '14.8'
        let installPath = '/srv/node/14.8'
        stepFormatToLatestPatchVersion.returns("v14.8.3")
        stepInstall.returns(nodeCacheDir)

        await install.installNode(inputJdkVersion, 'cnpm', installPath)

        sinon.assert.calledOnceWithExactly(rmSync, installPath, {recursive: true, force: true})
        sinon.assert.calledOnceWithMatch(symlinkSync, nodeCacheDir, installPath, 'dir')
    })
})

describe('installNpm', () => {
    it('should install cnpm when npmType is cnpm and under linux platform', async () => {
        sinon.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_LINUX)
        let cnpmCachedPath = "/tmp/cnpmCachedPath"
        stepInstall.returns(cnpmCachedPath)
        await install.installNpm('cnpm', '14.8.1', "nodeCachePath")
        sinon.assert.calledOnceWithExactly(addPath, `${cnpmCachedPath}/bin`)
        sinon.assert.notCalled(executeCommand);
        sinon.restore()
    })

    it('should install cnpm when npmType is cnpm and under darwin platform', async () => {
        sinon.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_DARWIN)
        await install.installNpm('cnpm', '14.8.1', "nodeCachePath")
        sinon.assert.calledWith(executeCommand, 'npm', ['install', '-g', 'cnpm@7.1.0', '--registry=https://registry.npmmirror.com']);
        sinon.assert.calledWith(executeCommandOutput, 'cnpm', ['-v'])
        sinon.restore()
    })

    it('should install cnpm when npmType is cnpm and under windows platform', async () => {
        sinon.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_WIN)
        sinon.stub(fs, 'mkdirSync').returns('/tmp/nodeCachePath/node_globa').returns('/tmp/nodeCachePath/node_cache')
        await install.installNpm('cnpm', '14.8.1', "/tmp/nodeCachePath")
        sinon.assert.calledWith(executeCommand, 'npm.cmd', ['config', 'set', 'prefix', '/tmp/nodeCachePath/node_global'])
        sinon.assert.calledWith(executeCommand, 'npm.cmd', ['config', 'set', 'cache', '/tmp/nodeCachePath/node_cache'])
        sinon.assert.calledWith(executeCommand, 'npm.cmd', ['install', '-g', 'cnpm@7.1.0', '--registry=https://registry.npmmirror.com']);
        sinon.restore()
    })

    it('should install specific version of pnpm depending on the version param', async () => {
        await install.installNpm('pnpm', '9.0.1', "nodeCachePath")
        sinon.assert.calledWith(executeCommand, 'npm', ['install', '-g', 'pnpm@2', '--registry=https://registry.npmmirror.com'])

        await install.installNpm('pnpm', '12.5.1', "nodeCachePath")
        sinon.assert.calledWith(executeCommand, 'npm', ['install', '-g', 'pnpm@5', '--registry=https://registry.npmmirror.com'])

        await install.installNpm('pnpm', '15.0.1', "nodeCachePath")
        sinon.assert.calledWith(executeCommand, 'npm', ['install', '-g', 'pnpm@7', '--registry=https://registry.npmmirror.com'])
    })

    it('should install specific version of pnpm depending on the version param when under windows platform', async () => {
        sinon.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_WIN)
        sinon.stub(fs, 'mkdirSync').returns('/tmp/nodeCachePath/node_globa').returns('/tmp/nodeCachePath/node_cache')
        await install.installNpm('pnpm', '9.0.1', "nodeCachePath")
        sinon.assert.calledWith(executeCommand, 'npm.cmd', ['install', '-g', 'pnpm@2', '--registry=https://registry.npmmirror.com'])

        await install.installNpm('pnpm', '12.5.1', "nodeCachePath")
        sinon.assert.calledWith(executeCommand, 'npm.cmd', ['install', '-g', 'pnpm@5', '--registry=https://registry.npmmirror.com'])

        await install.installNpm('pnpm', '15.0.1', "nodeCachePath")
        sinon.assert.calledWith(executeCommand, 'npm.cmd', ['install', '-g', 'pnpm@7', '--registry=https://registry.npmmirror.com'])
        sinon.restore()
    })
})

describe('isCnpmOrPnpmInstalled', () => {
    it('should return true when cnpm has isntalled', async () => {
        executeCommandOutput.resolves({ stdoutContent: `cnpm@7.1.0` })
        let result = await install.isCnpmOrPnpmInstalled('cnpm', '7.1.0')
        expect(result).to.be.true
        sinon.assert.calledWith(stepInfo, `cnpm@7.1.0 is already installed.`)

        executeCommandOutput.resolves({ stdoutContent: `7.1.0` })
        let result1 = await install.isCnpmOrPnpmInstalled('cnpm', '7.1.0')
        expect(result1).to.be.true
        sinon.assert.calledWith(stepInfo, `cnpm@7.1.0 is already installed.`)

        executeCommandOutput.resolves({ stdoutContent: `cnpm@7.1.0 (/opt/homebrew/lib/node_modules/cnpm/lib/parse_argv.js)
npm@6.14.18 (/opt/homebrew/lib/node_modules/cnpm/node_modules/npm/lib/npm.js)
node@22.0.0 (/opt/homebrew/Cellar/node/22.0.0/bin/node)
npminstall@5.8.1 (/opt/homebrew/lib/node_modules/cnpm/node_modules/npminstall/lib/index.js)
prefix=/opt/homebrew
darwin arm64 23.4.0
registry=https://registry.npmmirror.com` })
        let result2 = await install.isCnpmOrPnpmInstalled('cnpm', '7.1.0')
        expect(result2).to.be.true
        sinon.assert.calledWith(stepInfo, `cnpm@7.1.0 is already installed.`)
    })

    it('should return true when pnpm or pnpm has isntalled', async () => {
        executeCommandOutput.resolves({ stdoutContent: `7.33.7` })
        let result = await install.isCnpmOrPnpmInstalled('pnpm', '7')
        expect(result).to.be.true
        sinon.assert.calledWith(stepInfo, `pnpm@7 is already installed.`)
    })

    it('should return false if the command throws an error', async () => {
        executeCommandOutput.rejects(new Error('Command not found'));
        const result = await isCnpmOrPnpmInstalled('invalid-command', 'version');
        expect(result).to.be.false;
        sinon.assert.calledWith(stepInfo, `Error while checking invalid-command version: Error: Command not found`)
    })
})

describe('installYarn', () => {
    it('should install with registry and copy .yarnrc when useForeignCluster=false', async () => {
        sandbox.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_WIN)
        process.env.useForeignCluster = 'false';
        existsSyncStub.returns(false);

        await installYarn();
        expect(executeCommand.calledWithMatch('npm.cmd', [
            'install', 'yarn', '-g', '--registry=https://registry.npmmirror.com'
        ])).to.be.true;
        expect(copyFileSyncStub.calledWith(
            path.join(path.join(__dirname,'..','src'), '.yarnrc'),
            '.yarnrc'
        )).to.be.true;
    });

    it('should use existing .yarnrc when installed with registry', async () => {
        sandbox.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_LINUX)
        process.env.useForeignCluster = 'false';
        existsSyncStub.returns(true);

        await installYarn();

        expect(stepInfo.calledWith('found .yarnrc in the project dir')).to.be.true;
        expect(copyFileSyncStub.called).to.be.false;
    });

    it('should install without registry and skip .yarnrc check when useForeignCluster=true', async () => {
        sandbox.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_WIN)
        process.env.useForeignCluster = 'true';

        await installYarn();

        expect(executeCommand.calledWithMatch('npm.cmd', ['install', 'yarn', '-g'])).to.be.true;
        expect(existsSyncStub.called).to.be.false;
    });

    it('should handle copy failure gracefully', async () => {
        sandbox.stub(step.platform, 'getPlatform').returns(step.platform.PLATFORM_DARWIN)
        process.env.useForeignCluster = 'false';
        existsSyncStub.returns(false);
        const error = new Error('Failed to copy .yarnrc');
        copyFileSyncStub.throws(error);

        try{
            await installYarn();
            expect.fail()
        }catch (e){
            expect(e).to.equal(error);
        }
        expect(stepInfo.calledWith('install yarn successfully')).to.be.false;
    });
});
